/*
 * Copyright 2013 Sigurd Randoll.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.digiway.rapidbreeze.server.infrastructure.objectstorage;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;

/**
 * Some static helper methods for the {@linkplain ObjectStorage}.
 *
 * @author Sigurd
 */
public class ObjectStorageHelper {

    /**
     * Checks if the given object is an entity class. It checks if the class is
     * annotated with the {@linkplain Entity} annotation.
     *
     * @param obj
     * @return true if it is
     */
    public static boolean isEntity(Object obj) {
        return obj != null ? isEntity(obj.getClass()) : false;
    }

    /**
     * Checks if the given class is a valid entity class.
     *
     * @param clazz
     * @return true if it is
     */
    public static boolean isEntity(Class<?> clazz) {
        return clazz != null ? clazz.isAnnotationPresent(Entity.class) : false;
    }

    /**
     * Returns the table name of the given entity class. This might be the
     * default (canonial class name) or a specified value.
     *
     * @param obj
     * @return table name. never null
     */
    public static String getEntityTable(Class<?> objClass) {
        Entity entity = objClass.getAnnotation(Entity.class);
        if (entity == null || entity.table().isEmpty()) {
            return objClass.getCanonicalName();
        }
        return entity.table();
    }

    /**
     * Returns the table name of the given entity. See {@linkplain #getEntityTable(java.lang.Class)
     * }.
     *
     * @param obj
     * @return
     */
    public static String getEntityTable(Object obj) {
        return getEntityTable(obj.getClass());
    }

    /**
     * Retrieves the unique identifier of the given {@linkplain StorableObject}.
     *
     * @param obj
     * @return unique identifier
     * @throws IllegalStateException On any error while retrieving the
     * identifier.
     */
    public static String getId(Object obj) {
        Class<?> cls = obj.getClass();
        for (Field field : cls.getDeclaredFields()) {
            Id idAnnotation = field.getAnnotation(Id.class);
            if (idAnnotation != null) {
                if (!field.getType().isAssignableFrom(String.class)) {
                    throw new IllegalStateException("The field containing the unique identifier must be of type String.");
                }
                try {
                    if (!field.isAccessible()) {
                        field.setAccessible(true);
                    }

                    String id = (String) field.get(obj);
                    if (id == null || id.isEmpty()) {
                        throw new IllegalStateException("The field containing the unique identifier must not be empty.");
                    }
                    return id;
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new IllegalStateException(ex);
                }
            }
        }
        throw new IllegalStateException("Cannot retrieve field containing the unique identifier.");
    }

    /**
     * Checks if the given entity does have a valid id field.
     *
     * @param obj
     * @return
     */
    public static boolean hasValidIdField(Object obj) {
        Class<? extends Object> cls = obj.getClass();
        for (Field field : cls.getDeclaredFields()) {
            Id idAnnotation = field.getAnnotation(Id.class);
            if (idAnnotation != null) {
                if (!field.getType().isAssignableFrom(String.class)) {
                    return false;
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Checks if the given {@linkplain StorableObject} has a valid identifier.
     *
     * @param obj
     * @return true if it has
     */
    public static boolean hasValidId(Object obj) {
        Class<? extends Object> cls = obj.getClass();
        for (Field field : cls.getDeclaredFields()) {
            Id idAnnotation = field.getAnnotation(Id.class);
            if (idAnnotation != null) {
                if (!field.getType().isAssignableFrom(String.class)) {
                    return false;
                }
                try {
                    if (!field.isAccessible()) {
                        field.setAccessible(true);
                    }
                    String id = (String) field.get(obj);
                    if (id == null || id.isEmpty()) {
                        return false;
                    }
                    return true;
                } catch (IllegalArgumentException | IllegalAccessException ex) {
                    return false;
                }
            }
        }
        return false;
    }

    /**
     * Generates a unique identifier for the id field of the given entity.
     *
     * @param obj
     * @throws IllegalArgumentException if the entity class does not provide a
     * valid id field
     * @throws IllegalStateException if the identifier cannot be generated
     */
    public static void generateId(Object obj) {
        if (!hasValidIdField(obj)) {
            throw new IllegalArgumentException("The given entity " + obj + " does not have a valid id field.");
        }

        Class<? extends Object> cls = obj.getClass();
        for (Field field : cls.getDeclaredFields()) {
            Id idAnnotation = field.getAnnotation(Id.class);
            if (idAnnotation != null) {
                try {
                    if (!field.isAccessible()) {
                        field.setAccessible(true);
                    }
                    field.set(obj, UUID.randomUUID().toString());
                } catch (IllegalAccessException ex) {
                    throw new IllegalStateException("Cannot generate unique identifier for entity: " + obj, ex);
                }
            }
        }
    }

    /**
     * Checks if the given class does have a property with the given name.
     *
     * @param clazz
     * @param property
     * @return
     */
    public static boolean isPropertyAvailable(Class<?> clazz, String property) {
        return isGetterMethodAvailable(clazz, property) || isIsserMethodAvailable(clazz, property) || isFieldAvailable(clazz, property);
    }

    /**
     * Checks if the given object does have a property with the given name.
     *
     * @param obj
     * @param property
     * @return
     */
    public static boolean isPropertyAvailable(Object obj, String property) {
        return isPropertyAvailable(obj.getClass(), property);
    }

    private static boolean isFieldAvailable(Class<?> clazz, String property) {
        for (Field field : clazz.getDeclaredFields()) {
            if (field.getName().equals(property)) {
                return true;
            }
        }
        return false;
    }

    private static boolean isGetterMethodAvailable(Class<?> clazz, String property) {
        String getter = getGetterMethodName(property);
        for (Method method : clazz.getMethods()) {
            if (method.getParameterTypes().length == 0 && method.getName().equals(getter)) {
                return true;
            }
        }
        return false;
    }

    private static boolean isIsserMethodAvailable(Class<?> clazz, String property) {
        String isser = getIsserMethodName(property);
        for (Method method : clazz.getMethods()) {
            if (method.getParameterTypes().length == 0 && method.getName().equals(isser)) {
                return true;
            }
        }
        return false;

    }

    private static String getGetterMethodName(String property) {
        String getter = "get" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
        return getter;
    }

    private static String getIsserMethodName(String property) {
        String isser = "is" + Character.toUpperCase(property.charAt(0)) + property.substring(1);
        return isser;
    }

    /**
     * Tries to get the given property of the given object.
     *
     * @param <T>
     * @param object
     * @param property
     * @return
     */
    public static <T> T getProperty(Object object, String property) {
        if (!isPropertyAvailable(object.getClass(), property)) {
            throw new IllegalArgumentException("The given object:" + object + " does not provide the property:" + property);
        }
        Class<?> clazz = object.getClass();

        try {
            if (isGetterMethodAvailable(clazz, property)) {
                Method method = clazz.getMethod(getGetterMethodName(property));
                Object value = method.invoke(object);
                return (T) value;
            } else if (isIsserMethodAvailable(clazz, property)) {
                Method method = clazz.getMethod(getIsserMethodName(property));
                Object value = method.invoke(object);
                return (T) value;
            } else if (isFieldAvailable(clazz, property)) {
                Field field = clazz.getDeclaredField(property);
                field.setAccessible(true);
                Object value = field.get(object);
                return (T) value;
            }
        } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchFieldException | SecurityException ex) {
            throw new IllegalStateException("Cannot get property:" + property + " of object:" + object, ex);
        }
        throw new IllegalStateException("Cannot get property:" + property + " of object:" + object);
    }

    /**
     * Returns the class of the property with the given name
     *
     * @param clazz class to investigate
     * @param property name
     * @return
     */
    public static Class<?> getPropertyType(Class<?> clazz, String property) {
        if (!isPropertyAvailable(clazz, property)) {
            throw new IllegalArgumentException("The given class:" + clazz + " does not have the property:" + property);
        }

        try {
            if (isGetterMethodAvailable(clazz, property)) {
                Method method = clazz.getMethod(getGetterMethodName(property));
                return method.getReturnType();
            } else if (isIsserMethodAvailable(clazz, property)) {
                Method method = clazz.getMethod(getIsserMethodName(property));
                return method.getReturnType();
            } else if (isFieldAvailable(clazz, property)) {
                Field field = clazz.getDeclaredField(property);
                field.setAccessible(true);
                return field.getType();
            }
        } catch (NoSuchMethodException | IllegalArgumentException | NoSuchFieldException | SecurityException ex) {
            throw new IllegalStateException("Cannot get type of property:" + property + " of class:" + clazz, ex);
        }
        throw new IllegalStateException("Cannot get type of property:" + property + " of class:" + clazz);
    }
}
